/*
 * Copyright (C) 2010-2018 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */

package org.evosuite.testcase.localsearch;

import org.evosuite.Properties;
import org.evosuite.ga.ConstructionFailedException;
import org.evosuite.ga.localsearch.LocalSearchBudget;
import org.evosuite.ga.localsearch.LocalSearchObjective;
import org.evosuite.testcase.TestChromosome;
import org.evosuite.testcase.TestFactory;
import org.evosuite.testcase.statements.*;
import org.evosuite.testcase.variable.VariableReference;
import org.evosuite.utils.Randomness;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Gordon Fraser
 */
public class ReferenceLocalSearch extends StatementLocalSearch {

    private static final Logger logger = LoggerFactory.getLogger(ReferenceLocalSearch.class);

    private int positionDelta = 0;

    /* (non-Javadoc)
     * @see org.evosuite.testcase.LocalSearch#getPositionDelta()
     */
    @Override
    public int getPositionDelta() {
        return positionDelta;
    }


    private enum Mutations {
        REPLACE, PARAMETER, CALL
    }

    /* (non-Javadoc)
     * @see org.evosuite.testcase.LocalSearch#doSearch(org.evosuite.testcase.TestChromosome, int, org.evosuite.ga.LocalSearchObjective)
     */
    @Override
    public boolean doSearch(TestChromosome test, int statement,
                            LocalSearchObjective<TestChromosome> objective) {

        boolean hasImproved = false;
        int currentProbe = 0;

        backup(test);
        int oldLength = test.size();

        while (currentProbe < Properties.LOCAL_SEARCH_PROBES
                && !LocalSearchBudget.getInstance().isFinished()) {
            logger.info("Current probe on statement " + statement + ": " + currentProbe);

            List<Mutations> mutations = new ArrayList<>();
            mutations.add(Mutations.REPLACE);
            Statement st = test.getTestCase().getStatement(statement);
            if (!st.getReturnValue().isPrimitive() && !(st instanceof NullStatement)) {
                mutations.add(Mutations.CALL);
            }
            if (st.getNumParameters() > 0) {
                mutations.add(Mutations.PARAMETER);
            } else {
                mutations.remove(Mutations.PARAMETER);
            }

            int delta = 0;

            Mutations m = Randomness.choice(mutations);
            //logger.debug("Test before mutation: {}", test.getTestCase().toCode());

            switch (m) {
                case REPLACE:
                    replace(test, statement);
                    if (test.size() > oldLength)
                        delta = test.size() - oldLength;
                    break;
                case PARAMETER:
                    changeParameters(test, statement);
                    break;
                case CALL:
                    addCall(test, statement);
                    break;
            }

            if (test.isChanged()) {
                logger.info("Is changed");
                // logger.info("Test after mutation: " + test.getTestCase().toCode());
                if (objective.hasImproved(test)) {
                    logger.info("Fitness has improved, keeping");

                    currentProbe = 0;
                    hasImproved = true;
                    backup(test);
                    statement += delta;
                    positionDelta += delta;
                    oldLength = test.size();
                } else {
                    logger.info("Fitness has not improved, reverting");

                    currentProbe++;
                    restore(test);
                }
            } else {
                logger.info("Is not changed");
                currentProbe++;
            }

        }

        return hasImproved;
    }

    /**
     * Add a method call on the return value of the object at position statement
     *
     * @param test
     * @param statement
     */
    private boolean addCall(TestChromosome test, int statement) {

        logger.debug("Adding call");

        TestFactory factory = TestFactory.getInstance();
        Statement theStatement = test.getTestCase().getStatement(statement);
        VariableReference var = theStatement.getReturnValue();

        int oldLength = test.size();
        factory.insertRandomCallOnObjectAt(test.getTestCase(), var, statement + 1);
        test.setChanged(test.size() != oldLength);

        return false;
    }

    /**
     * Replace the call with a completely different call
     *
     * @param test
     * @param statement
     * @return
     */
    private boolean replace(TestChromosome test, int statement) {

        logger.debug("Replacing call");

        TestFactory factory = TestFactory.getInstance();
        Statement theStatement = test.getTestCase().getStatement(statement);
        VariableReference var = theStatement.getReturnValue();
        int oldLength = test.size();
        try {
            VariableReference replacement = null;
            if (Randomness.nextDouble() < Properties.NULL_PROBABILITY) {
                NullStatement nullStatement = new NullStatement(test.getTestCase(),
                        var.getType());
                replacement = test.getTestCase().addStatement(nullStatement, statement);
            } else if (!var.isPrimitive()) {
                // Test cluster does not keep track of generators for primitives
                replacement = factory.createObject(test.getTestCase(), var.getType(),
                        statement, 0, null);
            }
            if (replacement != null) {
                int oldStatement = statement + (test.size() - oldLength);
                for (int i = oldStatement + 1; i < test.size(); i++) {
                    test.getTestCase().getStatement(i).replace(var, replacement);
                }
                factory.deleteStatement(test.getTestCase(), oldStatement);
                test.setChanged(true);
            }

        } catch (ConstructionFailedException e) {
            if (test.size() < oldLength) {
                restore(test);
            }
            test.setChanged(test.size() != oldLength);
        }

        return false;
    }

    /**
     * Switch parameter/callee variables with other available objects
     *
     * @param test
     * @param statement
     * @return
     */
    private boolean changeParameters(TestChromosome test, int statement) {
        logger.debug("Changing parameters");
        Statement stmt = test.getTestCase().getStatement(statement);
        if (stmt instanceof MethodStatement) {
            return replaceMethodParameter(test, (MethodStatement) stmt);
        } else if (stmt instanceof ConstructorStatement) {
            return replaceConstructorParameter(test, (ConstructorStatement) stmt);
        } else if (stmt instanceof FieldStatement) {
            return replaceFieldSource(test, (FieldStatement) stmt);
        } else {
            return false;
        }

    }

    /**
     * Go through parameters of method call and apply local search
     *
     * @param test
     * @param statement
     */
    private boolean replaceMethodParameter(TestChromosome test, MethodStatement statement) {

        List<VariableReference> parameters = statement.getParameterReferences();
        if (parameters.isEmpty())
            return false;

        int max = parameters.size();
        if (!statement.isStatic()) {
            max++;
        }
        int numParameter = Randomness.nextInt(max);
        if (numParameter == parameters.size()) {
            // replace callee
            VariableReference callee = statement.getCallee();
            List<VariableReference> objects = test.getTestCase().getObjects(callee.getType(),
                    statement.getPosition());
            objects.remove(callee);
            if (objects.isEmpty())
                return false;

            VariableReference replacement = Randomness.choice(objects);
            statement.setCallee(replacement);
            test.setChanged(true);

        } else {
            VariableReference parameter = parameters.get(numParameter);
            List<VariableReference> objects = test.getTestCase().getObjects(parameter.getType(),
                    statement.getPosition());
            objects.remove(parameter);
            objects.remove(statement.getReturnValue());
            NullStatement nullStatement = new NullStatement(test.getTestCase(),
                    parameter.getType());
            if (!parameter.isPrimitive())
                objects.add(nullStatement.getReturnValue());

            if (objects.isEmpty())
                return false;

            VariableReference replacement = Randomness.choice(objects);
            if (replacement == nullStatement.getReturnValue()) {
                test.getTestCase().addStatement(nullStatement, statement.getPosition());
            }
            statement.replaceParameterReference(replacement, numParameter);
            test.setChanged(true);

        }

        return false;
    }

    /**
     * Go through parameters of constructor call and apply local search
     *
     * @param test
     * @param statement
     */
    private boolean replaceConstructorParameter(TestChromosome test,
                                                ConstructorStatement statement) {

        List<VariableReference> parameters = statement.getParameterReferences();
        if (parameters.isEmpty())
            return false;

        int numParameter = Randomness.nextInt(parameters.size());
        VariableReference parameter = parameters.get(numParameter);

        List<VariableReference> objects = test.getTestCase().getObjects(parameter.getType(),
                statement.getPosition());
        objects.remove(parameter);
        objects.remove(statement.getReturnValue());

        NullStatement nullStatement = new NullStatement(test.getTestCase(),
                parameter.getType());
        if (!parameter.isPrimitive())
            objects.add(nullStatement.getReturnValue());

        if (objects.isEmpty())
            return false;

        VariableReference replacement = Randomness.choice(objects);
        if (replacement == nullStatement.getReturnValue()) {
            test.getTestCase().addStatement(nullStatement, statement.getPosition());
        }

        statement.replaceParameterReference(replacement, numParameter);
        test.setChanged(true);

        return false;
    }

    /**
     * Try to replace source of field with all possible choices
     *
     * @param test
     * @param statement
     */
    private boolean replaceFieldSource(TestChromosome test, FieldStatement statement) {
        if (!statement.isStatic()) {
            VariableReference source = statement.getSource();
            List<VariableReference> objects = test.getTestCase().getObjects(source.getType(),
                    statement.getPosition());
            objects.remove(source);

            if (!objects.isEmpty()) {
                statement.setSource(Randomness.choice(objects));
                test.setChanged(true);
            }
        }

        return false;
    }

}
